/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2012  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.readers;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.sosy_lab.ccvisu.Options.Verbosity;
import org.sosy_lab.ccvisu.graph.Relation;
import org.sosy_lab.ccvisu.graph.Tuple;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * Extracts relations in RSF format from a given XML input.
 * BASEDON, COMPOUND, CONTAINEDIN, LOCATEDAT, MEMBER, REFERSTO
 * supported so far.
 */
public class ReaderDataGraphDOX extends ReaderDataGraph {

  private String mInputName;

  /**
   * Constructor.
   * @param in          Stream reader object.
   */
  public ReaderDataGraphDOX(BufferedReader in, Verbosity verbosity,
                            String inputName) {
    super(in, verbosity);
    this.mInputName = inputName;
  }

  /* (non-Javadoc)
   * @see org.sosy_lab.ccvisu.readers.ReaderDataGraph#readTuples()
   */
  @Override
  public Relation readTuples() {
    // This is where we store the relations.
    Relation lRelations = new Relation();

    XMLReader xmlReader = null;
    SAXHandlerDOXIndex indexFileHandler = null;
    try {
      // initialize xmlReader
      xmlReader = XMLReaderFactory.createXMLReader();
      // SAXParser saxParser =
      // SAXParserFactory.newInstance().newSAXParser();

      // update xmlReader for index-file parsing
      indexFileHandler = new SAXHandlerDOXIndex(lRelations);
      xmlReader.setContentHandler(indexFileHandler);
      xmlReader.setErrorHandler(indexFileHandler);

      // parse index file
      xmlReader.parse(new InputSource(reader));
      // saxParser.parse(new File(filePath + "/" + fileName),
      // filenamesDetector);
    } catch (SAXException e) {
      System.err.println("Runtime error: A SAX-error occured.");
      System.err.println(e.getMessage());
    } catch (IOException e) {
      System.err.println("Runtime error: Error while opening a file.");
      System.err.println(e.getMessage());
      System.exit(1);
    }

    // update xmlReader for xml-file parsing
    SAXHandlerDOXXml xmlFileHandler = new SAXHandlerDOXXml(lRelations);
    xmlReader.setContentHandler(xmlFileHandler);
    xmlReader.setErrorHandler(xmlFileHandler);

    // parse detected XML files
    List<String> xmlFilenames = indexFileHandler.getXmlFileIds();
    String lPath = ".";
    int lFileSepPos = mInputName.lastIndexOf(File.separator);
    if (lFileSepPos >= 0) {
      lPath = mInputName.substring(0, lFileSepPos);
    }
    for (String xmlFilename : xmlFilenames) {
      try {
        FileReader fileReader = new FileReader(lPath + File.separator
            + xmlFilename + ".xml");
        xmlReader.parse(new InputSource(fileReader));
        // saxParser.parse(new File(filePath + "/" + xmlFilenames.pop()
        // + ".xml"), fileParser);
        // Exception handling
      } catch (SAXException e) {
        System.err.println("Runtime error: A SAX-error occured.");
        System.err.println("  While parsing file '" + xmlFilename + "'.");
        System.err.println("  " + e.getMessage());
      } catch (IOException e) {
        System.err.println("Runtime error: Error while opening a file.");
        System.err.println(e.getMessage());
        System.exit(1);
      }
    }

    addExtraTuples(lRelations);

    return lRelations;
  }

  private void addExtraTuples(Relation relations) {
    // Construct mappings id-to-name, -kind, -containerName, and -file.
    Map<String, String> idToName = new HashMap<String, String>();
    Map<String, String> idToKind = new HashMap<String, String>();
    Map<String, String> idToContainerId = new HashMap<String, String>();
    Map<String, String> idToFile = new HashMap<String, String>();

    addExtraTuplesFirst(relations, idToName, idToKind, idToFile);

    addExtraTuplesSecond(relations, idToContainerId);

    addExtraTuplesThird(relations, idToName, idToKind, idToContainerId, idToFile);
  }

  private void addExtraTuplesThird(Relation relations,
      Map<String, String> idToName, Map<String, String> idToKind,
      Map<String, String> idToContainerId, Map<String, String> idToFile) {

    // Appending derived relations to pRelations.
    // A set for tracking which tuples we already added.
    for (Tuple tuple : new Relation(relations)) {

      String relationName = tuple.getRelationName();
      Iterator<String> it = tuple.getTupleElements().iterator();

      if (relationName.equals("REFERSTO")) {

        assert(tuple.getTupleElements().size() > 1);
        String source = it.next();
        String target = it.next();

        relations.addTuple("REF" + idToKind.get(target), source, target,
            idToName);

        String sourceContainer = idToContainerId.get(source);
        String targetContainer = idToContainerId.get(target);

        if (idToKind.get(sourceContainer).equals("class")
            && idToKind.get(targetContainer).equals("class")) {

          relations.addTuple("refClass", sourceContainer, targetContainer,
              idToName);
        }

        // We need some extra tolerance for file names.
        if (idToFile.get(source) == null) {
          System.err.println("Runtime warning: No file location found for "
              + "entry with id '" + source + "'.");
        } else if (idToFile.get(target) != null) {
          // Sometimes we don't get the file location for an id.
          // This happens, e.g., for 'enumvalue'. --- We omit such tuples.
          relations.addTuple("refFile", source, target, idToFile);
        }

      } else if (relationName.equals("BASEDON")) {

        assert(tuple.getTupleElements().size() > 1);
        String source = it.next();
        String target = it.next();

        if (!target.startsWith("UNKNOWN")) {
          if (idToKind.get(source).equals("class")) {
            if (idToKind.get(target).equals("class")) {
              relations.addTuple("inheritClass", source, target, idToName);
            }

            if (idToKind.get(target).equals("interface")) {
              relations.addTuple("implements", source, target, idToName);
            }
          }
        }
      }
    }
  }

  private void addExtraTuplesSecond(Relation relations,
      Map<String, String> idToContainerId) {

    // We must do a second loop over the tuples list, since we have to know all
    // of the classes' names beforehand
    for (Tuple tuple : relations) {
      String relationName = tuple.getRelationName();
      Iterator<String> it = tuple.getTupleElements().iterator();

      if (relationName.equals("CONTAINEDIN")) {
        assert (tuple.getTupleElements().size() == 2);

        String id = it.next().replace("\"", "");
        String name = it.next().replace("\"", "");
        String prev = idToContainerId.put(id, name);

        // Check for duplicate entries:
        if (prev != null
            && !prev.equals(tuple.getTupleElements().get(1))
            && verbosity.isAtLeast(Verbosity.WARNING)) {

          System.err.println("Warning: Multiple names found for id '"
              + tuple.getTupleElements().get(0)
              + "' (CONTAINEDIN):");
          System.err.println("    " + prev);
          System.err.println("    " + tuple.getTupleElements().get(1));
        }
      }
    }
  }

  private void addExtraTuplesFirst(Relation relations,
      Map<String, String> idToName, Map<String, String> idToKind,
      Map<String, String> idToFile) {

    for (Tuple tuple : relations) {
      String relationName = tuple.getRelationName();
      Iterator<String> it = tuple.getTupleElements().iterator();

      if (relationName.equals("MEMBER")
          || relationName.equals("COMPOUND")) {

        assert (tuple.getTupleElements().size() == 3);

        // TODO: Why do we use an iterator? Maybe we should use an ArrayList
        // in class Tuple and access the elements directly
        String relationKind = it.next();
        String id = it.next().replace("\"", "");
        String name = it.next().replace("\"", "");

        if (relationKind.equalsIgnoreCase("function")) {
          name += "()";
        }

        idToKind.put(id, relationKind);
        String lPrev = idToName.put(id, name);

        // Check for duplicate entries:
        if (lPrev != null
            && !lPrev.equals(tuple.getTupleElements().get(2))
            && verbosity.isAtLeast(Verbosity.WARNING)
            && !lPrev.equals(tuple.getTupleElements().get(2) + "()")) {

          System.err.println("Warning: Multiple names found for id '"
                             + tuple.getTupleElements().get(1) + "' (MEMBER/COMPOUND):");
          System.err.println("    " + lPrev);
          System.err.println("    " + tuple.getTupleElements().get(2));
        }

      } else if (relationName.equals("LOCATEDAT")) {
        assert (tuple.getTupleElements().size() == 3);
        String lPrev = idToFile.put(it.next(), it.next());

        // Skip the 'line-no'.
        // Check for duplicate entries:
        if (lPrev != null
            && !lPrev.equals(tuple.getTupleElements().get(1))
            && verbosity.isAtLeast(Verbosity.WARNING)) {

          System.err.println("Warning: Multiple locations found for id '"
              + tuple.getTupleElements().get(0)
              + "' (LOCATEDAT):");
          System.err.println("    " + lPrev);
          System.err.println("    " + tuple.getTupleElements().get(1));
        }
      }
    }
  }
}
